/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

package au.com.lastweekend.jim.io.exif;

import java.io.IOException;
import java.io.OutputStream;

import au.com.lastweekend.jim.io.metadata.MetadataException;
import au.com.lastweekend.jim.util.Rational;

/**
 * 
 * 
 * @todo make thus an enum
 * @version $Id: TiffTagFormat.java,v 1.2 2005/06/09 11:15:37 ggardner Exp $
 * @author <a href="mailto:grant@lastweekend.com.au"> <Grant Gardner> </a>
 */
@SuppressWarnings("unchecked")
public class TiffTagFormat {

    private static final TiffTagFormat INTEL_FORMAT;
    private static final TiffTagFormat MOTOROLA_FORMAT;

    private boolean isMotorolaByteOrder;
    public static final int FMT_DOUBLE = 12;
    public static final int FMT_SINGLE = 11;
    public static final int FMT_SRATIONAL = 10;
    public static final int FMT_SLONG = 9;
    public static final int FMT_SSHORT = 8;
    public static final int FMT_UNDEFINED = 7;
    public static final int FMT_SBYTE = 6;
    public static final int FMT_URATIONAL = 5;
    public static final int FMT_ULONG = 4;
    public static final int FMT_USHORT = 3;
    public static final int FMT_STRING = 2;
    public static final int FMT_BYTE = 1;
    private static final String BYTE = "BYTE";
    private static final String STRING = "STRING";
    private static final String USHORT = "USHORT";
    private static final String ULONG = "ULONG";
    private static final String URATIONAL = "URATIONAL";
    private static final String SBYTE = "SBYTE";
    private static final String UNDEFINED = "UNDEFINED";
    private static final String SSHORT = "SSHORT";
    private static final String SLONG = "SLONG";
    private static final String SRATIONAL = "SRATIONAL";
    private static final String SINGLE = "SINGLE";
    private static final String DOUBLE = "DOUBLE";

    private static final Class INT_ARRAY = int[].class;
    private static final Class BYTE_ARRAY = byte[].class;
    private static final Class RATIONAL_ARRAY = Rational[].class;

    /**
     * The number of bytes used per format descriptor.
     */
    private static final int[] BYTES_PER_FORMAT = {0, FMT_BYTE, FMT_BYTE, FMT_STRING, FMT_ULONG, FMT_SSHORT, FMT_BYTE, FMT_BYTE,
            FMT_STRING, FMT_ULONG, FMT_SSHORT, FMT_ULONG, FMT_SSHORT};
    /**
     * The number of formats known.
     */
    public static final int MAX_FORMAT_CODE = 12;

    public static String getNameFromFormatCode(int formatCode) throws MetadataException {

        switch (formatCode) {
            case FMT_BYTE :
                return BYTE;
            case FMT_STRING :
                return STRING;
            case FMT_USHORT :
                return USHORT;
            case FMT_ULONG :
                return ULONG;
            case FMT_URATIONAL :
                return URATIONAL;
            case FMT_SBYTE :
                return SBYTE;
            case FMT_UNDEFINED :
                return UNDEFINED;
            case FMT_SSHORT :
                return SSHORT;
            case FMT_SLONG :
                return SLONG;
            case FMT_SRATIONAL :
                return SRATIONAL;
            case FMT_SINGLE :
                return SINGLE;
            case FMT_DOUBLE :
                return DOUBLE;
            default :
                throw new MetadataException("value '" + formatCode + "' does not represent a known data format.");
        }
    }

    static {
        INTEL_FORMAT = new TiffTagFormat();
        INTEL_FORMAT.isMotorolaByteOrder = false;

        MOTOROLA_FORMAT = new TiffTagFormat();
        MOTOROLA_FORMAT.isMotorolaByteOrder = true;
    }

    public static TiffTagFormat getInstance(boolean isMotorolaByteOrder) {

        if (isMotorolaByteOrder) {
            return MOTOROLA_FORMAT;
        } else {
            return INTEL_FORMAT;
        }
    }

    public Object parse(byte[] data, int formatCode, int componentCount, int offset) throws TiffTagProcessingException {

        switch (formatCode) {
            case FMT_STRING :
                return readString(data, offset, componentCount);
            case TiffTagFormat.FMT_SRATIONAL :
            case TiffTagFormat.FMT_URATIONAL :
                // seems we don't treat signed and unsigned differently.
                if (componentCount == 1) {
                    return getRational(data, offset);
                } else {
                    return getRationals(data, componentCount, offset);
                }
            case TiffTagFormat.FMT_UNDEFINED :
            case TiffTagFormat.FMT_SBYTE :
            case TiffTagFormat.FMT_BYTE :
                // @TODO OK, so it was a mistake to make this a byte array, might as
                // well leave as ints...
                if (componentCount == 1) {
                    byte b = data[offset];
                    return Byte.valueOf(b);
                } else {
                    byte[] bytes = new byte[componentCount];
                    System.arraycopy(data, offset, bytes, 0, componentCount);
                    return bytes;
                }
            case TiffTagFormat.FMT_USHORT :
            case TiffTagFormat.FMT_SSHORT :
                if (componentCount == 1) {
                    int i = get16Bits(data, offset);
                    return i;
                } else {
                    int[] ints = new int[componentCount];
                    for (int i = 0; i < componentCount; i++) {
                        ints[i] = get16Bits(data, offset + (i * 2));
                    }
                    return ints;
                }
            case TiffTagFormat.FMT_SLONG :
            case TiffTagFormat.FMT_ULONG :
                if (componentCount == 1) {
                    int i = get32Bits(data, offset);
                    return i;
                } else {
                    int[] ints = new int[componentCount];
                    for (int i = 0; i < componentCount; i++) {
                        ints[i] = get32Bits(data, offset + (i * 4));
                    }
                    return ints;
                }
            default :
                throw new TiffTagProcessingException("unknown format code " + formatCode);
        }
    }

    public int getFormattedSize(Object tagValue, int formatCode) {

        int bytesPerFormat = getBytesPerFormat(formatCode);
        return getComponentCount(tagValue) * bytesPerFormat;
    }

    public int getComponentCount(Object tagValue) {

        int componentCount;
        if (tagValue.getClass().equals(Integer.class)) {
            componentCount = 1;

        } else if (tagValue.getClass().equals(INT_ARRAY)) {
            int[] tagValues = (int[]) tagValue;
            componentCount = tagValues.length;
        } else if (tagValue.getClass().equals(Byte.class)) {
            componentCount = 1;
        } else if (tagValue.getClass().equals(BYTE_ARRAY)) {
            byte[] tagValues = (byte[]) tagValue;
            componentCount = tagValues.length;
        } else if (tagValue.getClass().equals(Rational.class)) {
            componentCount = 1;
        } else if (tagValue.getClass().equals(RATIONAL_ARRAY)) {
            Rational[] tagValues = (Rational[]) tagValue;
            componentCount = tagValues.length;
        } else if (tagValue.getClass().equals(String.class)) {
            String tagString = (String) tagValue;
            // Add a "/0" to the end!
            componentCount = tagString.length() + 1;
        } else {
            throw new MetadataRuntimeException("Unknown tag object type" + tagValue.getClass());

        }

        return componentCount;
    }

    public void writeValue(Object tagValue, int formatCode, OutputStream output) throws IOException {

        if (tagValue.getClass().equals(Integer.class)) {
            writeInt(((Integer) tagValue).intValue(), formatCode, output);
        } else if (tagValue.getClass().equals(INT_ARRAY)) {
            int[] tagValues = (int[]) tagValue;
            for (int i = 0; i < tagValues.length; i++) {
                writeInt(tagValues[i], formatCode, output);
            }
        } else if (tagValue.getClass().equals(Byte.class)) {
            Byte byteValue = (Byte) tagValue;
            writeByte(byteValue.byteValue(), formatCode, output);
        } else if (tagValue.getClass().equals(BYTE_ARRAY)) {
            byte[] tagValues = (byte[]) tagValue;
            for (int i = 0; i < tagValues.length; i++) {
                writeByte(tagValues[i], formatCode, output);
            }
        } else if (tagValue.getClass().equals(Rational.class)) {
            writeRational((Rational) tagValue, formatCode, output);
        } else if (tagValue.getClass().equals(RATIONAL_ARRAY)) {
            Rational[] tagValues = (Rational[]) tagValue;
            for (int i = 0; i < tagValues.length; i++) {
                writeRational(tagValues[i], formatCode, output);
            }
        } else if (tagValue.getClass().equals(String.class)) {
            String tagString = (String) tagValue;
            writeString(tagString, formatCode, output);
        } else {
            throw new MetadataRuntimeException("Unknown tag object type" + tagValue.getClass());

        }

    }

    /**
     * @param tagString
     * @param formatCode
     * @param output
     * @throws IOException
     */
    private void writeString(String tagString, int formatCode, OutputStream output) throws IOException {

        output.write(tagString.getBytes());
        // Write a trailing zero.
        output.write(0x00);
    }

    /**
     * @param rational
     * @param formatCode
     * @param output
     * @throws IOException
     */
    private void writeRational(Rational rational, int formatCode, OutputStream output) throws IOException {

        write32bits(rational.getNumerator(), output);
        write32bits(rational.getDenominator(), output);

    }

    /**
     * @param i
     * @param formatCode
     * @param output
     * @throws IOException
     */
    private void writeInt(int i, int formatCode, OutputStream output) throws IOException {

        switch (formatCode) {
            case TiffTagFormat.FMT_USHORT :
            case TiffTagFormat.FMT_SSHORT :
                write16bits(i, output);
                break;
            case TiffTagFormat.FMT_SLONG :
            case TiffTagFormat.FMT_ULONG :
                write32bits(i, output);
                break;
            default :
                throw new MetadataRuntimeException("Bad format code: " + formatCode + "for int " + i);
        }

    }

    private void writeByte(byte byteValue, int formatCode, OutputStream output) throws IOException {

        switch (formatCode) {
            case FMT_UNDEFINED :
            case FMT_SBYTE :
            case FMT_BYTE :
                output.write(byteValue);
                break;

            default :
                throw new MetadataRuntimeException("Bad format code:" + formatCode + "for Byte" + byteValue);

        }
    }

    private Object getRationals(byte[] data, int componentCount, int offset) {

        Rational[] rationals = new Rational[componentCount];
        for (int i = 0; i < componentCount; i++) {
            int componentOffset = (8 * i);
            rationals[i] = getRational(data, offset + componentOffset);
        }
        return rationals;
    }

    private Rational getRational(byte[] data, int offset) {

        Rational rational = new Rational(get32Bits(data, offset), get32Bits(data, offset + 4));
        return rational;
    }

    /**
     * Creates a String from the _data buffer starting at the specified offset, and ending where byte=='\0' or where
     * length==maxLength.
     * 
     * @param data
     * @param offset
     * @param componentCount
     * @return
     */
    private String readString(byte[] data, int offset, int maxLength) {

        int length = 0;
        while ((offset + length) < data.length && data[offset + length] != '\0' && length < maxLength) {
            length++;
        }
        return new String(data, offset, length);
    }

    /**
     * @param formatCode
     * @return Number of Bytes used in format.
     */
    public int getBytesPerFormat(int formatCode) {

        return BYTES_PER_FORMAT[formatCode];
    }

    public int getByteCount(int formatCode, int componentCount) {

        int byteCount = componentCount * getBytesPerFormat(formatCode);
        return byteCount;
    }

    /**
     * 
     * @param data
     * @param offset
     * @return a 16 bit value from file's native byte order. Between 0x0000 and 0xFFFF.
     */
    public int get16Bits(byte[] data, int offset) {

        if (offset < 0 || offset >= data.length) {
            throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index " + offset
                    + " where max index is " + (data.length - 1) + ")");
        }
        if (isMotorolaByteOrder) {
            // Motorola big first
            return (data[offset] << 8 & 0xFF00) | (data[offset + 1] & 0xFF);
        } else {
            // Intel ordering
            return (data[offset + 1] << 8 & 0xFF00) | (data[offset] & 0xFF);
        }
    }

    /**
     * 
     * @param data
     * @param offset
     * @return a 32 bit value from file's native byte order.
     */
    public int get32Bits(byte[] data, int offset) {

        if (offset < 0 || offset >= data.length) {
            throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index " + offset
                    + " where max index is " + (data.length - 1) + ")");
        }
        if (isMotorolaByteOrder) {
            // Motorola big first
            return (data[offset] << 24 & 0xFF000000) | (data[offset + 1] << 16 & 0xFF0000) | (data[offset + 2] << 8 & 0xFF00)
                    | (data[offset + 3] & 0xFF);
        } else {
            // Intel ordering
            return (data[offset + 3] << 24 & 0xFF000000) | (data[offset + 2] << 16 & 0xFF0000) | (data[offset + 1] << 8 & 0xFF00)
                    | (data[offset] & 0xFF);
        }
    }

    public void write16bits(int value, OutputStream output) throws IOException {

        byte lowByte = (byte) (value & 0x00FF);
        byte highByte = (byte) ((value & 0xFF00) >> 8);

        if (isMotorolaByteOrder) {
            output.write(highByte);
            output.write(lowByte);
        } else {
            output.write(lowByte);
            output.write(highByte);
        }
    }

    public void write32bits(int value, OutputStream output) throws IOException {

        byte b1 = (byte) (value & 0x00FF);
        byte b2 = (byte) ((value & 0xFF00) >> 8);
        byte b3 = (byte) ((value & 0xFF0000) >> 16);
        byte b4 = (byte) ((value & 0xFF000000) >> 24);

        if (isMotorolaByteOrder) {
            output.write(b4);
            output.write(b3);
            output.write(b2);
            output.write(b1);
        } else {
            output.write(b1);
            output.write(b2);
            output.write(b3);
            output.write(b4);
        }

    }

    public boolean isMotorolaByteOrder() {

        return isMotorolaByteOrder;
    }
}